package git.soulbgm.security.service;

import git.soulbgm.security.config.AuthorityConfig;
import git.soulbgm.security.constant.SecurityConstants;
import git.soulbgm.security.pojo.LoginUser;
import git.soulbgm.utils.JsonUtil;
import git.soulbgm.utils.SequenceBuilder;
import git.soulbgm.utils.StringUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * token验证处理
 *
 * @author ruoyi
 */
@Component
public class TokenService {

    private final StringRedisTemplate redis;

    /**
     * 令牌自定义标识
     */
    private final String header;

    /**
     * 令牌秘钥
     */
    private final String secret;

    /**
     * 令牌有效期（默认30分钟）
     */
    private final int expireTime;

    /**
     * 同一用户允许登录次数(默认1) -1则代表不限制
     */
    private final Integer maximumSessions;

    /**
     * 当达到最多会话时是否禁止用户登录(默认false)
     */
    private final Boolean maxSessionsPreventsLogin;

    protected static final long MILLIS_SECOND = 1000;

    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

    private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;

    public TokenService(AuthorityConfig authorityConfig, StringRedisTemplate redis) {
        this.redis = redis;
        this.header = authorityConfig.getTokenHeader();
        this.secret = authorityConfig.getTokenSecret();
        this.expireTime = authorityConfig.getTokenExpireTime();
        this.maximumSessions = authorityConfig.getMaximumSessions();
        this.maxSessionsPreventsLogin = authorityConfig.getMaxSessionsPreventsLogin();
    }

    private <T> T get(String userKey, Class<T> tClass) {
        String json = redis.opsForValue().get(userKey);
        return JsonUtil.parse(json, tClass);
    }

    private Map<String, String> getMap(String key) {
        HashOperations<String, String, String> hashOperations = redis.opsForHash();
        return hashOperations.entries(key);
    }

    private void set(String key, String hashKey, String value, Integer timeout, TimeUnit timeUnit) {
        HashOperations<String, String, String> hashOperations = redis.opsForHash();
        hashOperations.put(key, hashKey, value);
        redis.expire(key, timeout, timeUnit);
    }

    private <T> void set(String key, T value, Integer timeout, TimeUnit timeUnit) {
        String json = JsonUtil.toJson(value);
        redis.opsForValue().set(key, json, timeout, timeUnit);
    }

    private Boolean delete(String key) {
        return redis.delete(key);
    }

    private Long delete(String key, String hashKey) {
        return redis.opsForHash().delete(key, hashKey);
    }

    public List<LoginUser> getAllLoginUser() {
        Set<String> keys = redis.keys(SecurityConstants.LOGIN_TOKEN_KEY + "*");
        if (keys == null) {
            return Collections.emptyList();
        }
        List<String> list = redis.opsForValue().multiGet(keys);
        if (list == null) {
            return Collections.emptyList();
        }
        List<LoginUser> userList = new ArrayList<>(list.size());
        for (String json : list) {
            userList.add(JsonUtil.parse(json, LoginUser.class));
        }
        return userList;
    }

    /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUser getLoginUser(HttpServletRequest request) {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtil.isNotEmpty(token)) {
            try {
                Claims claims = parseToken(token);
                // 解析对应的权限以及用户信息
                String uuid = (String) claims.get(SecurityConstants.LOGIN_USER_KEY);
                String userKey = getTokenKey(uuid);
                LoginUser user = get(userKey, LoginUser.class);
                return user;
            } catch (Exception e) {
            }
        }
        return null;
    }

    /**
     * 设置用户身份信息
     */
    public void setLoginUser(LoginUser loginUser) {
        if (StringUtil.isNotNull(loginUser) && StringUtil.isNotEmpty(loginUser.getToken())) {
            refreshToken(loginUser);
        }
    }

    /**
     * 删除用户身份信息
     */
    public void delLoginUser(String username, String token) {
        if (StringUtil.isNotEmpty(token)) {
            delete(getTokenKey(token));
            delete(getOnlineUserKey(username), token);
        }
    }

    /**
     * 检查是否可以登录
     *
     * @param username 用户名
     * @return boolean
     */
    public boolean checkIfCanLogin(String username) {
        //您的帐号在另一地点登录，您被迫下线(PC IP)
        //https://whois.pconline.com.cn/ipJson.jsp?ip=116.249.228.75&json=true
        if (maximumSessions < 0) {
            return true;
        }
        String onlineUserKey = getOnlineUserKey(username);
        Map<String, String> map = getMap(onlineUserKey);
        Integer loginCount = map.size();
        if (loginCount >= maximumSessions) {
            if (maxSessionsPreventsLogin) {
                return false;
            } else {
                Map.Entry<String, String> entry = map.entrySet().iterator().next();
                String token = entry.getKey();
                String ip = entry.getValue();
                delLoginUser(username, token);
            }
        }
        return true;
    }

    /**
     * 创建令牌
     *
     * @param loginUser 用户信息
     * @return 令牌
     */
    public String createToken(LoginUser loginUser) {
        String token = SequenceBuilder.uuid();
        loginUser.setToken(token);
        refreshToken(loginUser);

        Map<String, Object> claims = new HashMap<>();
        claims.put(SecurityConstants.LOGIN_USER_KEY, token);
        return createToken(claims);
    }

    /**
     * 验证令牌有效期，相差不足20分钟，自动刷新缓存
     *
     * @param loginUser
     * @return 令牌
     */
    public void verifyToken(LoginUser loginUser) {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
            refreshToken(loginUser);
        }
    }

    /**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshToken(LoginUser loginUser) {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String token = loginUser.getToken();
        String userKey = getTokenKey(token);
        set(userKey, loginUser, expireTime, TimeUnit.MINUTES);
        String onlineUserKey = getOnlineUserKey(loginUser.getUsername());
        set(onlineUserKey, token, loginUser.getLoginIpAddress(), expireTime, TimeUnit.MINUTES);
    }

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String createToken(Map<String, Object> claims) {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.getSubject();
    }

    /**
     * 获取请求token
     *
     * @param request
     * @return token
     */
    private String getToken(HttpServletRequest request) {
        String token = request.getHeader(header);
        if (StringUtil.isNotEmpty(token) && token.startsWith(SecurityConstants.TOKEN_PREFIX)) {
            token = token.replace(SecurityConstants.TOKEN_PREFIX, "");
        }
        return token;
    }

    private String getTokenKey(String uuid) {
        return SecurityConstants.LOGIN_TOKEN_KEY + uuid;
    }

    private String getOnlineUserKey(String username) {
        return SecurityConstants.ONLINE_USER_KEY + username;
    }

}
